/*
 * FileStorage.java
 *
 */

package org.atomojo.app.storage.file;

import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.atomojo.app.AtomResource;
import org.atomojo.app.Storage;
import org.atomojo.app.Storage.Query;
import org.atomojo.app.client.Feed;
import org.atomojo.app.client.Text;
import org.atomojo.app.db.DB;
import org.atomojo.app.db.DB.MediaEntryListener;
import org.atomojo.app.db.Entry;
import org.atomojo.app.db.EntryMedia;
import org.infoset.xml.Attribute;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.Item;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.XMLException;
import org.infoset.xml.util.WriterItemDestination;
import org.infoset.xml.util.XMLWriter;
import org.restlet.Application;
import org.restlet.Context;
import org.restlet.data.CharacterSet;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.FileRepresentation;
import org.restlet.representation.OutputRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.routing.Router;
import org.restlet.service.MetadataService;

/**
 *
 * @author alex
 */
public class FileStorage implements Storage
{
   public static final String FEED_DOCUMENT_NAME = ".feed.atom";
   static long syncWait = 5*60*1000;
   static {
      String value = System.getProperty("org.atomojo.app.storage.file.sync");
      if (value!=null) {
         syncWait = Long.parseLong(value);
      }
   }
   
   static MetadataService metaService = new MetadataService();

   DocumentLoader loader;
   File contentDir;
   Context context;
   DB db;
   boolean started = false;
   Thread syncThread;
   
   class SyncTask implements Runnable {
      public void run() {
         while (started) {
            try {
               sync();
               Thread.currentThread().sleep(syncWait);
            } catch (InterruptedException ex) {
            } catch (Exception ex) {
               getLogger().log(Level.SEVERE,"Error during synchronization.",ex);
            }
         }
      }
   }
   
   public FileStorage(DocumentLoader loader,DB db,File contentDir) {
      this.loader = loader;
      this.contentDir = contentDir;
      this.db = db;
   }

   protected Logger getLogger() {
      return context.getLogger();
   }
   
   public void start() 
      throws Exception
   {
      getLogger().info("Database "+db.getName()+" storing content in "+contentDir.getAbsolutePath());
      if (!contentDir.exists()) {
         contentDir.mkdirs();
      }
      started = true;
      syncThread = new Thread(new SyncTask());
      syncThread.start();
   }

   public void init(Context context)
   {
      this.context = context;
   }
   
   public void stop() 
      throws Exception
   {
      started = false;
      synchronized (syncThread) {
         syncThread.interrupt();
      }
      syncThread.join(2000);
   }

   public Application getAdministration() {
      return null;
   }
   
   public Representation getFeed(final String path,UUID id,final Iterator<Entry> entries)
      throws IOException
   {
      final File feedFile = makeCollectionReference(path);
      Representation rep = new OutputRepresentation(MediaType.APPLICATION_ATOM_XML) {
         public void write(OutputStream os)
            throws IOException
         {
            Writer out = new OutputStreamWriter(os,"UTF-8");
            ItemDestination dest = new WriterItemDestination(out,"UTF-8");
            FeedLoader feedLoader = new FeedLoader(getLogger(),loader,feedFile,entries);
            try {
               feedLoader.load(dest);
            } catch (XMLException ex) {
               throw new IOException("XML exception while loading feed: "+ex.getMessage());
            }
            out.flush();
         }
      };
      rep.setCharacterSet(CharacterSet.UTF_8);
      Date lastModified = new Date(makeFeedReference(path).lastModified());
      rep.setModificationDate(lastModified);
      return rep;
   }
   
   public Representation getFeedHead(String path,UUID id)
      throws IOException
   {
      return new FileRepresentation(makeFeedReference(path),MediaType.APPLICATION_ATOM_XML);
   }
   
   public boolean feedUpdated(String path,UUID feedId,Date updated)
      throws IOException
   {
      File feedFile = makeFeedReference(path);
      try {
         Document doc = loader.load(feedFile.toURI());
         Feed feed = new Feed(doc);
         feed.setUpdated(updated);
         Writer out = new OutputStreamWriter(new FileOutputStream(feedFile),"UTF-8");
         XMLWriter.writeDocument(doc, out);
         out.close();
         return true;
      } catch (XMLException ex) {
         throw new IOException("XML exception while updating: "+ex.getMessage());
      }
   }
   
   public String getFeedTitle(String path,UUID id) 
      throws IOException
   {
      File feedFile = makeFeedReference(path);
      try {
         Document doc = loader.load(feedFile.toURI());
         Feed feed = new Feed(doc);
         return feed.getTitle();
      } catch (XMLException ex) {
         throw new IOException("XML exception while getting title: "+ex.getMessage());
      }
   }
   
   public Status storeFeed(String path,UUID id,Document doc)
   {
      File feedFile = makeFeedReference(path);
      File dir = feedFile.getParentFile();
      if (!dir.exists()) {
         if (!dir.mkdirs()) {
            getLogger().log(Level.SEVERE,"Cannot make parent directory for feed: "+dir.getAbsolutePath());
            return Status.SERVER_ERROR_INTERNAL;
         }
      }
      try {
         Writer out = new OutputStreamWriter(new FileOutputStream(feedFile),"UTF-8");
         XMLWriter.writeDocument(doc, out);
         out.close();
         return Status.SUCCESS_OK;
      } catch (Exception ex) {
         getLogger().log(Level.SEVERE,"Cannot write feed document to "+feedFile.getAbsolutePath(),ex);
         return Status.SERVER_ERROR_INTERNAL;
      }
   }
   
   public boolean deleteFeed(String path, UUID id)
   {
      File dir = makeCollectionReference(path);
      final List<File> queue = new ArrayList<File>();
      queue.add(dir);
      boolean ok = true;
      int mark = -1;
      while (ok && queue.size()>0) {
         File target = queue.remove(queue.size()-1);
         if (target.isDirectory()) {
            if (mark==queue.size()) {
               ok = target.delete();
               mark = -1;
            } else {
               mark = queue.size();
               queue.add(target);
               target.listFiles(new FileFilter() {
                  public boolean accept(File f)
                  {
                     queue.add(f);
                     return false;
                  }
               });
            }
         } else {
            ok = target.delete();
         }
      }
      return ok;
   }
   
   public Status storeEntry(String path,UUID feedId,UUID id,Document entryDoc)
      throws IOException
   {
      File entryFile = makeEntryReference(path,id);
      try {
         Writer out = new OutputStreamWriter(new FileOutputStream(entryFile),"UTF-8");
         XMLWriter.writeDocument(entryDoc, out);
         out.close();
         return Status.SUCCESS_OK;
      } catch (Exception ex) {
         getLogger().log(Level.SEVERE,"Cannot write entry document to "+entryFile.getAbsolutePath(),ex);
         return Status.SERVER_ERROR_INTERNAL;
      }
      
   }
   
   public Representation getEntry(final String feedBaseURI,String path,UUID feedId,UUID id)
      throws IOException
   {
      final File entry = makeEntryReference(path,id);
      Representation rep = new OutputRepresentation(MediaType.APPLICATION_ATOM_XML) {
         public void write(OutputStream os)
            throws IOException
         {
            Writer w = new OutputStreamWriter(os,"UTF-8");
            final WriterItemDestination dest = new WriterItemDestination(w,"UTF-8",true);
            dest.setOmitXMLDeclaration(true);
            try {
               loader.generate(entry.toURI(), new ItemDestination() {
                  int level = 0;
                  public void send(Item item)
                     throws XMLException
                  {
                     switch (item.getType()) {
                        case ElementItem:
                           if (level==0) {
                              Element e = (Element)item;
                              e.setAttributeValue(Attribute.XML_BASE, feedBaseURI);
                           } else {
                              Element e = (Element)item;
                              e.setBaseURI(null);
                           }
                           level++;
                           break;
                        case ElementEndItem:
                           level--;
                     }
                     dest.send(item);
                  }
               });
            } catch (XMLException ex) {
               throw new IOException(ex.getMessage());
            }
         }
      };
      rep.setCharacterSet(CharacterSet.UTF_8);
      rep.setModificationDate(new Date(entry.lastModified()));
      return rep;
   }
   
   public boolean deleteEntry(String path,UUID feedId,UUID id)
   {
      File entryFile = makeEntryReference(path,id);
      return entryFile.delete();
   }
   
   public Status storeMedia(String path,UUID feedId,String name,MediaType type,InputStream data)
      throws IOException
   {
      File mediaFile = makeMediaReference(path,name);
      
      OutputStream os = new FileOutputStream(mediaFile);
      byte [] buffer = new byte[8196];
      
      int len;
      while ((len=data.read(buffer))>0) {
         os.write(buffer,0,len);
      }
      os.flush();
      os.close();
      
      return Status.SUCCESS_CREATED;
   }
   
   public Representation getMedia(String path,UUID feedId,String name)  
      throws IOException
   {
      File mediaFile = makeMediaReference(path,name);
      return new FileRepresentation(mediaFile,MediaType.APPLICATION_OCTET_STREAM);
   }
   
   public Representation getMediaHead(String path,UUID feedId,String name) 
      throws IOException
   {
      File mediaFile = makeMediaReference(path,name);
      Representation rep = new StringRepresentation("",MediaType.APPLICATION_OCTET_STREAM);
      rep.setModificationDate(new Date(mediaFile.lastModified()));
      return rep;
   }
   
   public boolean deleteMedia(String path,UUID feedId,String name)
   {
      File mediaFile = makeMediaReference(path,name);
      return mediaFile.delete();
   }
   
   public Query getQuery(String path, UUID feedId, String name) 
      throws IOException
   {
      throw new IOException("Queries are not supported by file storage.");
   }
   
   public Query compileQuery(String query)
      throws IOException
   {
      throw new IOException("Queries are not supported by file storage.");
   }

   public Representation queryFeed(String path,UUID feedId,Query query,Map<String,String> parameters)
      throws IOException
   {
      throw new IOException("Queries are not supported by file storage.");
   }
   
   public Representation queryCollection(String path,UUID feedId,Query query,Map<String,String> parameters)
      throws IOException
   {
      throw new IOException("Queries are not supported by file storage.");
   }
   
   
   protected File makeEntryReference(String path,UUID entryId) {
      return new File(new File(contentDir,path),"."+entryId.toString()+".atom");
   }
   
   protected File makeMediaReference(String path,String name) {
      return new File(new File(contentDir,path),name);
   }
   
   protected File makeFeedReference(String path) {
      //log.info("contentDir="+contentDir+", path="+path);
      return new File(new File(contentDir,path),FEED_DOCUMENT_NAME);
   }
   
   protected File makeCollectionReference(String path) {
      return new File(contentDir,path);
   }
   
   public void sync() 
      throws SQLException
   {
      getLogger().info("Synchronizing "+contentDir);
      Iterator<org.atomojo.app.db.Feed> feeds = db.getFeeds();
      while (feeds.hasNext()) {
         org.atomojo.app.db.Feed feed = feeds.next();
         if (feed==null) {
            continue;
         }
         File dir = new File(contentDir,feed.getPath());
         if (dir.lastModified()>feed.getSynchronizedAt().getTime()) {
            getLogger().info("Changed detected in "+dir+" "+dir.lastModified()+">"+feed.getEdited().getTime());
            sync(feed,dir);
         }
      }
   }
   
   protected void sync(final org.atomojo.app.db.Feed feed,final File dir)
      throws SQLException
   {
      final AtomicBoolean ok = new AtomicBoolean(true);
      
      // scan for new feeds and entries
      dir.listFiles(new FileFilter() {
         public boolean accept(File file) {
            String name = file.getName();
            if (file.isDirectory()) {
               try {
                  org.atomojo.app.db.Feed child = feed.getChild(name);
                  if (child==null) {
                     importDir(feed,file);
                  }
               } catch (Exception ex) {
                  getLogger().log(Level.SEVERE,"Error while importing directory "+file.getAbsolutePath(),ex);
                  ok.set(false);
               }
            } else {
               if (name.charAt(0)=='.') {
                  if (!name.equals(".feed.atom") && name.endsWith(".atom")) {
                     name = name.substring(1);
                     name = name.substring(0,name.length()-5);
                     try {
                        UUID id = UUID.fromString(name);
                        Entry entry = feed.findEntry(id);
                        if (entry==null) {
                           importEntry(feed,file);
                        }
                     } catch (IllegalArgumentException ex) {
                        getLogger().warning("Ignoring entry-like file: "+file.getName());
                        ok.set(false);
                     } catch (Exception ex) {
                        getLogger().log(Level.SEVERE,"Error while import entry "+file.getName(),ex);
                        ok.set(false);
                     }
                  }
               }
            }
            return false;
         }
      });
      dir.listFiles(new FileFilter() {
         public boolean accept(File file) {
            String name = file.getName();
            if (!file.isDirectory() && name.charAt(0)!='.') {
               try {
                  EntryMedia media = feed.findEntryResource(name);
                  if (media==null) {
                     importMedia(feed,file);
                  }
               } catch (Exception ex) {
                  getLogger().log(Level.SEVERE,"Error while importing media "+file.getAbsolutePath(),ex);
                  ok.set(false);
               }
            }
            return false;
         }
      });
      Iterator<org.atomojo.app.db.Feed> children = feed.getChildren();
      while (children.hasNext()) {
         org.atomojo.app.db.Feed child = children.next();
         File childDir = new File(dir,child.getName());
         if (!childDir.exists()) {
            child.delete();
         }
      }
      Iterator<Entry> entries = feed.getEntries();
      while (entries.hasNext()) {
         Entry entry = entries.next();
         File entryFile = new File(dir,"."+entry.getUUID()+".atom");
         boolean delete = false;
         if (entryFile.exists()) {
            Iterator<EntryMedia> resources = entry.getResources();
            while (!delete && resources.hasNext()) {
               File media = new File(dir,resources.next().getName());
               if (!media.exists()) {
                  delete = true;
               }
            }
         } else {
            delete = true;
         }
         if (delete) {
            entry.delete(new MediaEntryListener() {
               public void onDelete(EntryMedia resource) {
                  File media = new File(dir,resource.getName());
                  if (media.exists()) {
                     media.delete();
                  }
               }
            });
            if (entryFile.exists()) {
               entryFile.delete();
            }
         }
      }
      if (ok.get()) {
         feed.markSynchronized();
      }
   }
   
   protected void importDir(org.atomojo.app.db.Feed parent,File dir)
      throws SQLException,IOException,XMLException
   {
      
      // check for feed file
      File feedFile = new File(dir,".feed.atom");
      org.atomojo.app.db.Feed feed = null;
      if (feedFile.exists()) {
         
         // exists, so we'll import it
         Document doc = loader.load(feedFile.toURI());
         Feed feedObj = new Feed(doc);
         String idS = feedObj.getId();
         if (!idS.startsWith("urn:uuid:")) {
            throw new IOException("Bad feed id: "+idS);
         }
         try {
            UUID id = UUID.fromString(idS.substring(9));
            feed = parent.createChild(dir.getName(), id);
         } catch (IllegalArgumentException ex) {
            throw new IOException(ex.getMessage());
         }
      } else {
         
         // create new feed
         feed = parent.createChild(dir.getName());
         
         // create feed document
         Document doc = AtomResource.createFeedDocument(dir.getName(),feed.getUUID(),feed.getCreated());
         Writer out = new OutputStreamWriter(new FileOutputStream(feedFile),"UTF-8");
         XMLWriter.writeDocument(doc, out);
         out.close();
      }
      
      // import directory
      sync(feed,dir);
   }
   
   protected void importEntry(final org.atomojo.app.db.Feed parent,File file)
      throws SQLException,IOException,XMLException
   {
      Document doc = loader.load(file.toURI());
      org.atomojo.app.client.Entry entryObj = new org.atomojo.app.client.Entry(doc);
      String idS = entryObj.getId();
      if (!idS.startsWith("urn:uuid:")) {
         throw new IOException("Bad entry id: "+idS);
      }
      try {
         UUID id = UUID.fromString(idS.substring(9));
         
         // check for entry-related media
         Text text = entryObj.getContent();
         File media = null;
         if (text!=null) {
            String src = text.getSourceLink();
            media = new File(file.getParentFile(),src);
            if (!media.exists()) {
               throw new IOException("Cannot find related media "+src+" to entry "+file.getAbsolutePath());
            }
         }
         
         // create entry
         Entry entry = parent.createEntry(id);
         
         if (media!=null) {
            // create entry media
            MediaType type = MediaType.APPLICATION_OCTET_STREAM;
            int dot = media.getName().lastIndexOf('.');
            if (dot>0) {
               String ext = media.getName().substring(dot+1);
               type = MediaType.valueOf(metaService.getMetadata(ext).getName());
            }
            entry.createResource(media.getName(), type);
         }

         // edit entry date
         entryObj.setEdited(entry.getEdited());
         Writer out = new OutputStreamWriter(new FileOutputStream(file),"UTF-8");
         XMLWriter.writeDocument(doc, out);
         out.close();

         // feed was edited
         parent.edited();
         feedUpdated(parent.getPath(),parent.getUUID(),entry.getEdited());
      } catch (IllegalArgumentException ex) {
         throw new IOException(ex.getMessage());
      }
   }
   
   protected void importMedia(final org.atomojo.app.db.Feed parent,File file)
      throws SQLException,IOException,XMLException
   {
      // get media type
      String name = file.getName();
      MediaType type = MediaType.APPLICATION_OCTET_STREAM;
      int dot = name.lastIndexOf('.');
      if (dot>0) {
         String ext = name.substring(dot+1);
         type = MediaType.valueOf(metaService.getMetadata(ext).getName());
      }
      
      // create entry for media
      Entry entry = parent.createEntry();
      
      // create entry media
      entry.createResource(name, type);
      
      // creae entry document
      Document doc = AtomResource.createMediaEntryDocument(name,entry.getUUID(),entry.getCreated(),null,name,type);
      Writer out = new OutputStreamWriter(new FileOutputStream(new File(file.getParentFile(),"."+entry.getUUID()+".atom")),"UTF-8");
      XMLWriter.writeDocument(doc, out);
      out.close();
      
      // feed was edited
      parent.edited();
      feedUpdated(parent.getPath(),parent.getUUID(),entry.getEdited());
   }
}
